/*
* Sonatype Nexus (TM) Open Source Version
* Copyright (c) 2008-present Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
* which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
package org.sonatype.nexus.rapture.internal.security;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.sonatype.nexus.common.text.Strings2;
import org.sonatype.nexus.common.wonderland.AuthTicketService;
import org.sonatype.nexus.extdirect.DirectComponentSupport;
import org.sonatype.nexus.rapture.StateContributor;
import org.sonatype.nexus.security.Roles;
import org.sonatype.nexus.security.SecuritySystem;
import org.sonatype.nexus.security.anonymous.AnonymousConfiguration;
import org.sonatype.nexus.security.anonymous.AnonymousManager;
import org.sonatype.nexus.security.authz.WildcardPermission2;
import org.sonatype.nexus.security.privilege.Privilege;
import org.sonatype.nexus.validation.Validate;
import com.codahale.metrics.annotation.ExceptionMetered;
import com.codahale.metrics.annotation.Timed;
import com.softwarementors.extjs.djn.config.annotations.DirectAction;
import com.softwarementors.extjs.djn.config.annotations.DirectMethod;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.subject.Subject;
import org.hibernate.validator.constraints.NotEmpty;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
/**
* Security Ext.Direct component.
*
* @since 3.0
*/
@Named
@Singleton
@DirectAction(action = "rapture_Security")
public class SecurityComponent
extends DirectComponentSupport
implements StateContributor
{
private final SecuritySystem securitySystem;
private final AnonymousManager anonymousManager;
private final AuthTicketService authTickets;
@Inject
public SecurityComponent(final SecuritySystem securitySystem,
final AnonymousManager anonymousManager,
final AuthTicketService authTickets)
{
this.securitySystem = checkNotNull(securitySystem);
this.anonymousManager = checkNotNull(anonymousManager);
this.authTickets = checkNotNull(authTickets);
}
// FIXME: Move authenticate to session servlet
@DirectMethod
@Timed
@ExceptionMetered
@Validate
public UserXO authenticate(@NotEmpty final String base64Username, @NotEmpty final String base64Password)
throws Exception
{
Subject subject = securitySystem.getSubject();
// FIXME: Subject is not nullable, but we have code that checks for nulls, likely from testing setups, verify and simplify
checkState(subject != null);
try {
subject.login(new UsernamePasswordToken(
Strings2.decodeBase64(base64Username),
Strings2.decodeBase64(base64Password),
false
));
}
catch (Exception e) {
throw new Exception("Authentication failed", e);
}
return getUser();
}
@DirectMethod
@Timed
@ExceptionMetered
@Validate
public String authenticationToken(@NotEmpty final String base64Username, @NotEmpty final String base64Password)
throws Exception
{
Subject subject = securitySystem.getSubject();
if (subject == null || !subject.isAuthenticated()) {
authenticate(base64Username, base64Password);
}
String username = Strings2.decodeBase64(base64Username);
String password = Strings2.decodeBase64(base64Password);
log.debug("Authenticate w/username: {}, password: {}", username, Strings2.mask(password));
// Require current user to be the requested user to authenticate
subject = securitySystem.getSubject();
if (!subject.getPrincipal().toString().equals(username)) {
throw new Exception("Username mismatch");
}
// Ask the sec-manager to authenticate, this won't alter the current subject
try {
SecurityUtils.getSecurityManager().authenticate(new UsernamePasswordToken(username, password));
}
catch (AuthenticationException e) {
throw new Exception("Authentication failed", e);
}
// At this point we should be authenticated, return a new ticket
return authTickets.createTicket();
}
@DirectMethod
@Timed
@ExceptionMetered
public UserXO getUser() {
UserXO userXO = null;
Subject subject = securitySystem.getSubject();
if (isLoggedIn(subject)) {
userXO = new UserXO();
userXO.setAuthenticated(subject.isAuthenticated());
// HACK: roles for the current user are not exposed to the UI.
// HACK: but we need to know if user is admin or not for some things (like outreach)
if (subject.hasRole(Roles.ADMIN_ROLE_ID)) {
userXO.setAdministrator(true);
}
Object principal = subject.getPrincipal();
if (principal != null) {
userXO.setId(principal.toString());
AnonymousConfiguration anonymousConfiguration = anonymousManager.getConfiguration();
if (anonymousConfiguration.isEnabled() && userXO.getId().equals(anonymousConfiguration.getUserId())) {
userXO = null;
}
}
}
return userXO;
}
@DirectMethod
@Timed
@ExceptionMetered
public List<PermissionXO> getPermissions() {
List<PermissionXO> permissions = null;
Subject subject = securitySystem.getSubject();
if (isLoggedIn(subject)) {
permissions = calculatePermissions(subject);
}
return permissions;
}
@Override
public Map<String, Object> getState() {
Map<String, Object> state = new HashMap<>();
state.put("user", getUser());
state.put("permissions", getPermissions());
AnonymousConfiguration anonymousConfiguration = anonymousManager.getConfiguration();
state.put("anonymousUsername", anonymousConfiguration.isEnabled() ? anonymousConfiguration.getUserId() : null);
return state;
}
private boolean isLoggedIn(final Subject subject) {
return subject != null && (subject.isRemembered() || subject.isAuthenticated());
}
// FIXME: Avoid calculating permissions for every poll request
private List<PermissionXO> calculatePermissions(final Subject subject) {
log.debug("Calculating permissions");
List<Permission> granted = new ArrayList<>();
List<PermissionXO> result = new ArrayList<>();
// find all privileges which we expose the UI, , which we can deconstruct and evaluate
for (Privilege privilege : securitySystem.listPrivileges()) {
// only WildcardPermission2 presently is supported due to toString() implementation
if (privilege.getPermission() instanceof WildcardPermission2) {
granted.add(privilege.getPermission());
}
}
// determine which of the exposed privilege permissions the current subject is granted
boolean[] boolResults = subject.isPermitted(granted);
for (int i = 0; i < granted.size(); i++) {
if (boolResults[i]) {
PermissionXO entry = new PermissionXO();
entry.setId(granted.get(i).toString());
result.add(entry);
}
}
// FIXME: Permissions must be sorted for state-hash calculation :-(
Collections.sort(result, new Comparator<PermissionXO>()
{
@Override
public int compare(final PermissionXO o1, final PermissionXO o2) {
return o1.getId().compareTo(o2.getId());
}
});
return result;
}
}